// Behavior originally contributed by Chinajade.
//
// LICENSE:
// This work is licensed under the
//     Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
// also known as CC-BY-NC-SA.  To view a copy of this license, visit
//      http://creativecommons.org/licenses/by-nc-sa/3.0/
// or send a letter to
//      Creative Commons // 171 Second Street, Suite 300 // San Francisco, California, 94105, USA.
//

#region Summary and Documentation
// DOCUMENTATION:
//      http://www.thebuddyforum.com/mediawiki/index.php?title=Honorbuddy_Custom_Behavior:_PullMobs
//
// QUICK DOX:
//      Collects items from mobs or objects when (right-click) 'interaction' is required.
//      Most useful for those type of quests where you blow something up,
//      then you have to collect the pieces.
//
//  Parameters (required, then optional--both listed alphabetically):
//      (***One or more of the following two attributes must be specified***)
//      MobIdN [REQUIRED if ObjectId is omitted]: Defines the mobs that drop the Items we're after.
//              N may be omitted, or any numeric value--multiple mobs are supported.
//      ObjectIdN [REQUIRED if MobId is omitted]: Defines the objects that drop the Items we're after.
//              N may be omitted, or any numeric value--mulitple objects are supported.
//
//      (This attribute is optional, but governs what other attributes are optional)
//      CollectUntil [Default: RequiredCountReached]: Defines the terminating condition for
//              this behavior.  Available options include:  NoTargetsInArea, RequiredCountReached, QuestComplete.
//              "Targets" means mobs or objects--whatever is dropping the items we're after.
//
//      (***These attributes may/may not be optional based on value of CollectUntil attribute***)
//      CollectItemCount [REQUIRED if CollectUntil=RequiredCountReached; Default: 1]:
//              represents the number of items we must collect for the behavior to terminate.
//      CollectItemId [REQUIRED if CollectUntil=NoTargetsInArea or RequiredCountReached; Default:none]:
//              Identifies the item we are collecting.  The only time this attribute may be omitted
//              is when we're collecting intangibles such as 'attitudes' or 'liberations' that
//              will complete the quest.
//      QuestId [REQUIRED if CollectUntil=QuestComplete; Default:none]:
//
//      (***These attibutes are completely optional***)
//      HuntingGroundRadius [Default: 120]: The range from the anchor location (i.e., X/Y/Z) location at which
//              targets (mobs or objects) will be sought.
//      IgnoreMobsInBlackspots [Default: false]: If true, mobs sitting in blackspotted areas will not be
//              considered as targets.
//      MobState [Default: DontCare]: Identifies the state in which the Mob must be to be considered
//              as a target.  The MobState only applies if the target is some form of NPC.  The MobState
//              Valid values are Alive/Dead/DontCare.
//      NonCompeteDistance [Default: 25]: If a player is within this distance of a target that looks
//              interesting to us, we'll ignore the target.  The assumption is that the player may
//              be going for the same target, and we don't want to draw attention.
//      PostInteractDelay [Default: 1500ms]: The number of milliseconds to wait after each interaction.
//              This is useful if the target requires time for the interaction to complete.
//              This value must be on the closed interval [0..61000].
//      QuestCompleteRequirement [Default:NotComplete]:
//      QuestInLogRequirement [Default:InLog]:
//              A full discussion of how the Quest* attributes operate is described in
//              http://www.thebuddyforum.com/mediawiki/index.php?title=Honorbuddy_Programming_Cookbook:_QuestId_for_Custom_Behaviors
//      X/Y/Z [Default: Toon's initial position]: Defines the anchor of a search area for
//              which targets (mobs or objects) will be sought.  The hunting ground is defined by
//              this value coupled with the CollectionDistance.
// 
#endregion


#region Examples
// Exmaples:
// <CustomBehavior File="PullMobs" ...other args... >
//     <Hotspot Name="Cathedral Square fishing dock" X="4554.003" Y="-4718.743" Z="883.0464" StartPoint="true" />
//     <Hotspot Name="The Shady Lady" X="4578.725" Y="-4721.257" Z="882.8724" />
//     <Hotspot Name="The Blue Recluse" X="4584.166" Y="-4693.487" Z="882.7331" StartPoint="true" />
// </CustomBehavior>
// 
#endregion


#region Usings
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using CommonBehaviors.Actions;
using Honorbuddy.QuestBehaviorCore;
using Styx;
using Styx.CommonBot;
using Styx.CommonBot.Frames;
using Styx.CommonBot.Profiles;
using Styx.Pathing;
using Styx.Plugins;
using Styx.TreeSharp;
using Styx.WoWInternals;
using Styx.WoWInternals.WoWObjects;
using Styx.WoWInternals.World;

using Action = Styx.TreeSharp.Action;
#endregion


namespace Honorbuddy.Quest_Behaviors.Cava.PullMobs
{
    [CustomBehaviorFileName(@"Cava\PullMobs")]
	public class PullMobs : CustomForcedBehavior
	{
		public enum CollectUntilType
		{
			NoTargetsInArea,
			RequiredCountReached,
			QuestComplete,
		}

		public enum MobStateType
		{
			Alive,
			Dead,
			DontCare,
		}

		public PullMobs(Dictionary<string, string> args)
			: base(args)
		{
			QBCLog.BehaviorLoggingContext = this;

			try
			{
				bool isCollectItemCountRequired = false;
				bool isCollectItemIdRequired = false;
				bool isQuestIdRequired = false;

				CollectUntil = GetAttributeAsNullable<CollectUntilType>("CollectUntil", false, null, null) ?? CollectUntilType.RequiredCountReached;
				if ((CollectUntil == CollectUntilType.NoTargetsInArea)
					|| (CollectUntil == CollectUntilType.RequiredCountReached))
				{
					isCollectItemCountRequired = true;
					isCollectItemIdRequired = true;
				}
				else if (CollectUntil == CollectUntilType.QuestComplete)
				{ isQuestIdRequired = true; }

				CollectItemCount = GetAttributeAsNullable<int>("CollectItemCount", isCollectItemCountRequired, ConstrainAs.CollectionCount, null) ?? 1;
				CollectItemId = GetAttributeAsNullable<int>("CollectItemId", isCollectItemIdRequired, ConstrainAs.ItemId, null) ?? 0;
				HuntingGroundAnchor = GetAttributeAsNullable<WoWPoint>("", false, ConstrainAs.WoWPointNonEmpty, null) ?? Me.Location;
				HuntingGroundRadius = GetAttributeAsNullable<double>("HuntingGroundRadius", false, new ConstrainTo.Domain<double>(1.0, 200.0), new[] { "CollectionDistance" }) ?? 120.0;
				IgnoreMobsInBlackspots = GetAttributeAsNullable<bool>("IgnoreMobsInBlackspots", false, null, null) ?? false;
				MobIds = GetNumberedAttributesAsArray<int>("MobId", 0, ConstrainAs.MobId, null);
				MobState = GetAttributeAsNullable<MobStateType>("MobState", false, null, null) ?? MobStateType.DontCare;
				NonCompeteDistance = GetAttributeAsNullable<double>("NonCompeteDistance", false, new ConstrainTo.Domain<double>(1.0, 150.0), null) ?? 25.0;
				ObjectIds = GetNumberedAttributesAsArray<int>("ObjectId", 0, ConstrainAs.ObjectId, null);
				PostInteractDelay = TimeSpan.FromMilliseconds(GetAttributeAsNullable<int>("PostInteractDelay", false, new ConstrainTo.Domain<int>(0, 61000), null) ?? 1500);
				RandomizeStartingHotspot = GetAttributeAsNullable<bool>("RandomizeStartingHotspot", false, null, null) ?? false;
				QuestId = GetAttributeAsNullable<int>("QuestId", isQuestIdRequired, ConstrainAs.QuestId(this), null) ?? 0;
				QuestRequirementComplete = GetAttributeAsNullable<QuestCompleteRequirement>("QuestCompleteRequirement", false, null, null) ?? QuestCompleteRequirement.NotComplete;
				QuestRequirementInLog = GetAttributeAsNullable<QuestInLogRequirement>("QuestInLogRequirement", false, null, null) ?? QuestInLogRequirement.InLog;
                // Added by Cava
                PullMob = GetAttributeAsNullable<bool>("PullMob", false, null, null) ?? false;


				// Semantic coherency --
				if ((MobIds.Count() <= 0) && (ObjectIds.Count() <= 0))
				{
					QBCLog.Error("You must specify one or more MobId(s) or ObjectId(s)");
					IsAttributeProblem = true;
				}

				if (HuntingGroundRadius < (NonCompeteDistance * 2))
				{
					QBCLog.Error("The CollectionDistance (saw '{0}') must be at least twice the size"
								+ " of the NonCompeteDistance (saw '{1}').",
								HuntingGroundRadius,
								NonCompeteDistance);
					IsAttributeProblem = true;
				}

                // Added by Cava
                if (PullMob && Me.Level < 1)
                {
                    LogMessage("error", "Must be over level 15 to enable PullMob");
                    IsAttributeProblem = true;
                }
                if (PullMob && (MobIds.Count() <= 0))
                {
                    LogMessage("error", "You must specify one or more MobId(s) to enable PullMob");
                    IsAttributeProblem = true;
                }


				// Find the item name --
				ItemInfo itemInfo = ItemInfo.FromId((uint)CollectItemId);

				ItemName = (itemInfo != null) ? itemInfo.Name : string.Format("Item({0})", CollectItemId);


				// Sub-behaviors...
				_behavior_SwimBreath = new SwimBreathBehavior();
				_behavior_HuntingGround = new HuntingGroundBehavior(IsViableTarget,
																	HuntingGroundAnchor,
																	HuntingGroundRadius);
				_behavior_UnderwaterLooting = new UnderwaterLootingBehavior();
			}

			catch (Exception except)
			{
				// Maintenance problems occur for a number of reasons.  The primary two are...
				// * Changes were made to the behavior, and boundary conditions weren't properly tested.
				// * The Honorbuddy core was changed, and the behavior wasn't adjusted for the new changes.
				// In any case, we pinpoint the source of the problem area here, and hopefully it
				// can be quickly resolved.
				QBCLog.Exception(except);
				IsAttributeProblem = true;
			}
		}


		// Attributes provided by caller
		public int CollectItemCount { get; private set; }
		public int CollectItemId { get; private set; }
		public CollectUntilType CollectUntil { get; private set; }
		public WoWPoint HuntingGroundAnchor { get; private set; }
		public double HuntingGroundRadius { get; private set; }
		public bool IgnoreMobsInBlackspots { get; private set; }
		public int[] MobIds { get; private set; }
		public MobStateType MobState { get; private set; }
		public double NonCompeteDistance { get; private set; }
		public int[] ObjectIds { get; private set; }
		public TimeSpan PostInteractDelay { get; private set; }
		public bool RandomizeStartingHotspot { get; private set; }
		public int QuestId { get; private set; }
		public QuestCompleteRequirement QuestRequirementComplete { get; private set; }
		public QuestInLogRequirement QuestRequirementInLog { get; private set; }
        // Added by Cava
        public bool PullMob { get; private set; }

		// Private properties and data...
		private HuntingGroundBehavior _behavior_HuntingGround;
		private SwimBreathBehavior _behavior_SwimBreath;
		private UnderwaterLootingBehavior _behavior_UnderwaterLooting;
		private bool _isBehaviorDone = false;
		private PluginContainer _pluginAntiDrown;
		private bool _pluginAntiDrownWasEnabled;

		private WoWObject CurrentTarget { get { return (_behavior_HuntingGround.CurrentTarget); } }
		private readonly TimeSpan Delay_MobConsumedExpiry = TimeSpan.FromMinutes(7);
		private readonly TimeSpan Delay_BlacklistPlayerTooClose = TimeSpan.FromSeconds(90);
		private TimeSpan Delay_WowClientLagTime { get { return (TimeSpan.FromMilliseconds((StyxWoW.WoWClient.Latency * 2) + 150)); } }
		private string ItemName { get; set; }
		private static LocalPlayer Me { get { return (StyxWoW.Me); } }

		// Private LINQ queries..
		private int CollectedItemCount
		{
			get
			{
				return ((int)Me.BagItems
							.Where(item => (item.ItemInfo.Id == CollectItemId))
							.Sum(item => item.StackCount));
			}
		}

		// DON'T EDIT THESE--they are auto-populated by Subversion
		public override string SubversionId { get { return ("$Id: PullMobs.cs 1738 2014-10-15 19:47:44Z chinajade $"); } }
		public override string SubversionRevision { get { return ("$Revision: 1738 $"); } }



		// If player is close to a target that is interesting to us, ignore the target...
		// The player may be going for the same mob, and we don't want to draw attention.
		// We'll blacklist the mob for a bit, in case the player is running around, or following
		// us.  The excaption is ithe player is in our party, then we can freely kill any target
		// close to him.
		private bool BlacklistIfPlayerNearby(WoWObject target)
		{
			WoWUnit nearestCompetingPlayer = ObjectManager.GetObjectsOfType<WoWUnit>(true, false)
													.OrderBy(player => player.Location.Distance(target.Location))
													.FirstOrDefault(player => player.IsPlayer
																				&& player.IsAlive
																				&& !player.IsInOurParty());

			// If player is too close to the target, ignore target for a bit...
			if ((nearestCompetingPlayer != null)
				&& (nearestCompetingPlayer.Location.Distance(target.Location) <= NonCompeteDistance))
			{
				target.LocallyBlacklist(Delay_BlacklistPlayerTooClose);
				return (true);
			}

			return (false);
		}


		private void GuiShowProgress(string completionReason)
		{
			TreeRoot.GoalText = string.Format("{0}: {1}/{2} {3}", this.GetType().Name, CollectedItemCount, CollectItemCount, ItemName);

			if (completionReason != null)
			{
				QBCLog.DeveloperInfo("Behavior done (" + completionReason + ")");
				TreeRoot.GoalText = string.Empty;
				TreeRoot.StatusText = string.Empty;
			}
		}


		public bool IsViableTarget(WoWObject target)
		{
			bool isViable;

			if (target == null)
				{ return (false); }

			isViable = (target.IsValid
						&& (MobIds.Contains((int)target.Entry) || ObjectIds.Contains((int)target.Entry))
						&& !target.IsLocallyBlacklisted()
						&& !BlacklistIfPlayerNearby(target)
						&& Query.IsStateMatch_IgnoreMobsInBlackspots(target, IgnoreMobsInBlackspots));

			if (isViable && (target is WoWUnit))
			{
				WoWUnit wowUnit = target.ToUnit();

				isViable = ((wowUnit.IsAlive && (MobState == MobStateType.Alive))
							|| (wowUnit.IsDead && (MobState == MobStateType.Dead))
							|| (MobState == MobStateType.DontCare));
			}

			return (isViable);
		}


		private void ParseHuntingGroundHotspots(bool randomizeStartingHotspot)
		{
			List<WoWPointNamed> huntingGroundHotspots = new List<WoWPointNamed>();

			foreach (XElement element in Element.Elements().Where(elem => (elem.Name == "Hotspot")))
			{
				double? x = ParseXmlElementDouble(element, "X", true);
				double? y = ParseXmlElementDouble(element, "Y", true);
				double? z = ParseXmlElementDouble(element, "Z", true);

				if (!x.HasValue || !y.HasValue || !z.HasValue)
				{ continue; }

				bool isStarting = ParseXmlElementBool(element, "StartPoint", false) ?? false;
				string name = ParseXmlElementString(element, "Name", false);

				huntingGroundHotspots.Add(new WoWPointNamed(new WoWPoint(x.Value, y.Value, z.Value), name, isStarting));
			}

			_behavior_HuntingGround.UseHotspots(huntingGroundHotspots, randomizeStartingHotspot);
		}


		private bool? ParseXmlElementBool(XElement element,
											string attributeName,
											bool isRequired)
		{
			string location = (((IXmlLineInfo)element).HasLineInfo()
										? (" @line " + ((IXmlLineInfo)element).LineNumber.ToString())
										: string.Empty);
			bool tmpBool;

			if (element.Attribute(attributeName) == null)
			{
				if (isRequired)
				{
					QBCLog.Error("Hotspot{0} is missing the '{1}' attribute (required)", location, attributeName);
					IsAttributeProblem = true;
				}
				return (null);
			}

			if (!bool.TryParse(element.Attribute(attributeName).Value, out tmpBool))
			{
				QBCLog.Error("Hotspot{0} '{1}' attribute is malformed", location, attributeName);
				IsAttributeProblem = true;
				return (null);
			}

			return (tmpBool);
		}


		private double? ParseXmlElementDouble(XElement element,
											  string attributeName,
											  bool isRequired)
		{
			string location = (((IXmlLineInfo)element).HasLineInfo()
										? (" @line " + ((IXmlLineInfo)element).LineNumber.ToString())
										: string.Empty);
			double tmpDouble;

			if (element.Attribute(attributeName) == null)
			{
				if (isRequired)
				{
					QBCLog.Error("Hotspot{0} is missing the '{1}' attribute (required)", location, attributeName);
					IsAttributeProblem = true;
				}
				return (null);
			}

			if (!double.TryParse(element.Attribute(attributeName).Value, out tmpDouble))
			{
				QBCLog.Error("Hotspot{0} '{1}' attribute is malformed", location, attributeName);
				IsAttributeProblem = true;
				return (null);
			}

			return (tmpDouble);
		}


		private string ParseXmlElementString(XElement element,
											  string attributeName,
											  bool isRequired)
		{
			string location = (((IXmlLineInfo)element).HasLineInfo()
										? (" @line " + ((IXmlLineInfo)element).LineNumber.ToString())
										: string.Empty);

			if (element.Attribute(attributeName) == null)
			{
				if (isRequired)
				{
					QBCLog.Error("Hotspot{0} is missing the '{1}' attribute (required)", location, attributeName);
					IsAttributeProblem = true;
				}
				return (null);
			}

			return (element.Attribute(attributeName).Value);
		}


		#region Overrides of CustomForcedBehavior

		protected override Composite CreateBehavior()
		{
			return (
				new PrioritySelector(
				// If behavior done, bail...
				// Note that this is also an implicit "is quest complete" exit criteria, also.
					new Decorator(ret => IsDone,
						new Action(delegate { GuiShowProgress("quest complete"); })),

					// If we've filled our inventory quota, we're done...
					new Decorator(
						ret => (CollectedItemCount >= CollectItemCount),
						new Action(delegate
							{
								GuiShowProgress(string.Format("{0}/{1} items collected", CollectedItemCount, CollectItemCount));
								_isBehaviorDone = true;
							})),

					// If we're dead, the behavior can't function so bail until alive...
					new Decorator(ret => Me.IsDead, new ActionAlwaysSucceed()),

					// If swimming, check if we need breath...
					_behavior_SwimBreath.CreateBehavior(),

					// If there is loot to clean up...
					new Decorator(context => LootTargeting.LootMobs,
						_behavior_UnderwaterLooting.CreateBehavior(() => true)),

					// Find next target...
					_behavior_HuntingGround.CreateBehavior_SelectTarget(() => (CollectUntil == CollectUntilType.NoTargetsInArea)),

					// If no target and that's our exit criteria, we're done...
					new Decorator(ret => ((CurrentTarget == null)
										  && (CollectUntil == CollectUntilType.NoTargetsInArea)),
						new Action(delegate
						{
							GuiShowProgress("No more objects/mobs in area");
							_isBehaviorDone = true;
						})),

					// Otherwise, keep the unit of interest targeted...
					new Decorator(ret => ((Me.CurrentTarget != CurrentTarget) && (CurrentTarget is WoWUnit)),
						new Action(delegate
						{
							CurrentTarget.ToUnit().Target();
							return (RunStatus.Failure);     // Fall through
						})),

					// Keep progress updated...
					new Action(delegate
					{
						GuiShowProgress(null);
						return (RunStatus.Failure);     // Fall through
					}),

                    //Added by Cava
                    new Decorator(ret => PullMob && !Me.IsCasting && CurrentTarget.Location.Distance(Me.Location) < 30,
                        new Action(context =>
                            {
                                var spell = "";
                                switch (Me.Class)
                                {
                                        case WoWClass.Mage:
                                            spell = "Ice Lance";
                                            break;
                                        case WoWClass.Druid:
                                            spell = "Moonfire";
                                            break;
                                        case WoWClass.Paladin:
                                            spell = "Judgment";
                                            break;
                                        case WoWClass.Priest:
                                            spell = "Shadow Word: Pain";
                                            break;
                                        case WoWClass.Shaman:
                                            spell = "Flame Shock";
                                            break;
                                        case WoWClass.Warlock:
                                            spell = "Corruption";
                                            break;
                                        case WoWClass.DeathKnight:
                                            spell = "Dark Command";
                                            break;
                                        case WoWClass.Hunter:
                                            spell = "Arcane Shot";
                                            break;
                                        case WoWClass.Warrior:
                                            if (SpellManager.CanCast("Shoot"))
                                                spell = "Shoot";
                                            if (SpellManager.CanCast("Throw"))
                                                spell = "Throw";
                                            break;
                                        case WoWClass.Rogue:
                                            if (SpellManager.CanCast("Shoot"))
                                                spell = "Shoot";
                                            if (SpellManager.CanCast("Throw"))
                                                spell = "Throw";
                                            break;
                                        case WoWClass.Monk:
                                            spell = "Provoke";
                                            break;
                                }
                                WoWMovement.MoveStop();
                                CurrentTarget.ToUnit().Target();
                                CurrentTarget.ToUnit().Face();
                                SpellManager.Cast(spell);
                            }
                    )),
                    // If we're not at target, move to it...
					_behavior_HuntingGround.CreateBehavior_MoveToTarget(),

					// We're within interact range, collect the object...
					new Sequence(
						new Action(delegate { WoWMovement.MoveStop(); }),
						new Action(delegate { _behavior_HuntingGround.MobEngaged(CurrentTarget); }),
						new WaitContinue(Delay_WowClientLagTime, ret => false, new ActionAlwaysSucceed()),
						new Action(delegate { CurrentTarget.Interact(); }),
						new WaitContinue(Delay_WowClientLagTime, ret => false, new ActionAlwaysSucceed()),
						new WaitContinue(PostInteractDelay, ret => false, new ActionAlwaysSucceed()),
						new Action(delegate { CurrentTarget.LocallyBlacklist(Delay_MobConsumedExpiry); }),
						new Action(delegate { Me.ClearTarget(); })
						)
				)
			);
		}


        
        public override void OnFinished()
        {
            TreeRoot.GoalText = string.Empty;
            TreeRoot.StatusText = string.Empty;
			if (_pluginAntiDrown != null)
			{
				_pluginAntiDrown.Enabled = _pluginAntiDrownWasEnabled;
				_pluginAntiDrown = null;
			}
            base.OnFinished();
        }

		public override bool IsDone
		{
			get
			{
				return (_isBehaviorDone     // normal completion
						|| !UtilIsProgressRequirementsMet(QuestId, QuestRequirementInLog, QuestRequirementComplete));
			}
		}


		public override void OnStart()
		{
			// The XML element didn't exist when the constructor was called...
			// So we had to defer some final parsing that really should've happened in the constructor
			// to the OnStart() method.  This will parse the final arguments, and set IsAttributeProblem
			// correctly, for normal processing.
			ParseHuntingGroundHotspots(RandomizeStartingHotspot);

			// This reports problems, and stops BT processing if there was a problem with attributes...
			// We had to defer this action, as the 'profile line number' is not available during the element's
			// constructor call.
			OnStart_HandleAttributeProblem();

			// If the quest is complete, this behavior is already done...
			// So we don't want to falsely inform the user of things that will be skipped.
			if (!IsDone)
			{
				// Disable the AntiDrown plugin if present, as it interferes with our anti-drown prevention...
				_pluginAntiDrown = PluginManager.Plugins.FirstOrDefault(plugin => (plugin.Name == "Anti-Drown"));
				if (_pluginAntiDrown != null)
				{
					_pluginAntiDrownWasEnabled = _pluginAntiDrown.Enabled;
					_pluginAntiDrown.Enabled = false;
				}

				this.UpdateGoalText(QuestId);

				GuiShowProgress(null);
			}
		}

		#endregion
	}


	public class WoWPointNamed
	{
		public WoWPointNamed(WoWPoint location,
							 string name,
							 bool isStarting)
		{
			Location = location;
			Name = (!string.IsNullOrEmpty(name) ? name : location.ToString());
			IsStarting = isStarting;
		}


		public WoWPointNamed(WoWPoint location,
							 string name)
			: this(location, name, false)
		{
			// empty
		}

		public WoWPointNamed(WoWPoint location)
			: this(location, null, false)
		{
			// empty
		}

		public WoWPointNamed()
			: this(WoWPoint.Empty, null, false)
		{
			// empty
		}

		public WoWPoint Location { get; set; }
		public string Name { get; set; }
		public bool IsStarting { get; set; }
	}


	#region Reusable behaviors
	// The behaviors in this section were designed to be efficient and robust.
	// The robustness results in some larger-than-normal, but swift code.
	// We also designed them to be reused in other behaviors--just copy, paste,
	// and call them as-needed.

	public class HuntingGroundBehavior
	{
		public delegate bool BehaviorFailIfNoTargetsDelegate();
		public delegate double DistanceDelegate();
		public delegate bool IsViableTargetDelegate(WoWObject target);
		public delegate WoWPointNamed LocationDelegate();
		public delegate void LoggerDelegate(string messageType, string format, params object[] args);
		public delegate WoWObject WoWObjectDelegate();


		public HuntingGroundBehavior(IsViableTargetDelegate isViableTarget,
									 WoWPoint huntingGroundAnchor,
									 double collectionDistance)
		{
			CollectionDistance = collectionDistance;
			HuntingGroundAnchor = new WoWPointNamed(huntingGroundAnchor, "Hunting Ground Anchor", true);
			IsViableTarget = isViableTarget;

			// UseHotspots(null, false);
		}


		public void MobEngaged(WoWObject wowObject)
		{
			if (wowObject == CurrentTarget)
			{ _currentTargetAutoBlacklistTimer.Stop(); }
		}


		// Public properties...
		public double CollectionDistance { get; private set; }
		public WoWObject CurrentTarget { get; private set; }
		public WoWPointNamed HuntingGroundAnchor { get; private set; }
		public IsViableTargetDelegate IsViableTarget { get; private set; }


		// Private properties & data...
		private const string AuraName_DruidAquaticForm = "Aquatic Form";
		private readonly TimeSpan Delay_AutoBlacklist = TimeSpan.FromMinutes(7);
		private readonly TimeSpan Delay_RepopWait = TimeSpan.FromMilliseconds(500);
		private readonly TimeSpan Delay_StatusUpdateThrottle = TimeSpan.FromMilliseconds(1000);
		private readonly TimeSpan Delay_WoWClientMovementThrottle = TimeSpan.FromMilliseconds(0);
		private TimeSpan Delay_WowClientLagTime { get { return (TimeSpan.FromMilliseconds((StyxWoW.WoWClient.Latency * 2) + 150)); } }
		private static LocalPlayer Me { get { return (StyxWoW.Me); } }
		private const double MinDistanceToUse_DruidAquaticForm = 27.0;
		private int SpellId_DruidAquaticForm = 1066;
		private IEnumerable<WoWObject> ViableTargets()
		{
			return (ObjectManager.GetObjectsOfType<WoWObject>(true, false)
					.Where(target => IsViableTarget(target)
									 && (target.Distance < CollectionDistance))
					.OrderBy(target => (Me.IsSwimming
										? target.Distance
										: Me.Location.SurfacePathDistance(target.Location))));
		}

		private TimeSpan _currentTargetAutoBlacklistTime = TimeSpan.FromSeconds(1);
		private readonly Stopwatch _currentTargetAutoBlacklistTimer = new Stopwatch();
		private Queue<WoWPointNamed> _hotSpots = new Queue<WoWPointNamed>();
		private WoWPointNamed _huntingGroundWaitPoint = new WoWPointNamed(WoWPoint.Empty,
																							"Hunting Ground Wait Point");
		private readonly Stopwatch _repopWaitingTime = new Stopwatch();


		/// <summary>
		/// The created behavior was meant to be used in a PrioritySelector.
		/// It may also have uses inside other TreeSharp Composites.
		/// </summary>
		/// 
		/// <returns>
		/// <para>* RunStatus.Failure, if current target is viable.
		/// It will also return Failure if no targets could be located and failIfNoTargets is true</para>
		/// <para>* RunStatus.Success, if acquiring a target (or waiting for them to repop)</para>
		/// </returns>
		///
		public Composite CreateBehavior_SelectTarget()
		{
			return (CreateBehavior_SelectTarget(() => false));
		}

		public Composite CreateBehavior_SelectTarget(BehaviorFailIfNoTargetsDelegate failIfNoTargets)
		{
			return (
			new PrioritySelector(

				// If we haven't engaged the mob when the auto-blacklist timer expires, give up on it and move on...
				new Decorator(ret => ((CurrentTarget != null)
										&& (_currentTargetAutoBlacklistTimer.Elapsed > _currentTargetAutoBlacklistTime)),
					new Action(delegate
					{
                        QBCLog.Warning("Taking too long to engage '{0}'--blacklisting", CurrentTarget.SafeName);
						CurrentTarget.LocallyBlacklist(Delay_AutoBlacklist);
						CurrentTarget = null;
					})),


				// If we don't have a current target, select a new one...
				// Once we select a target, its 'locked in' (unless it gets blacklisted).  This prevents us
				// from running back and forth between two equidistant targets.
				new Decorator(ret => ((CurrentTarget == null)
									  || !CurrentTarget.IsValid
									  || CurrentTarget.IsLocallyBlacklisted()
									  || !IsViableTarget(CurrentTarget)),
					new PrioritySelector(context => CurrentTarget = ViableTargets().FirstOrDefault(),

						// If we found next target, we're done...
						new Decorator(ret => (CurrentTarget != null),
							new Action(delegate
							{
								_huntingGroundWaitPoint.Location = WoWPoint.Empty;

								if (CurrentTarget is WoWUnit)
								{ CurrentTarget.ToUnit().Target(); }

								_currentTargetAutoBlacklistTime = CalculateAutoBlacklistTime(CurrentTarget);
								_currentTargetAutoBlacklistTimer.Reset();
								_currentTargetAutoBlacklistTimer.Start();
							})),

						// If we've exhausted mob/object supply in area, and we need to wait, do so...
						new Decorator(ret => !failIfNoTargets(),

							// Move back to hunting ground anchor --
							new PrioritySelector(

								// If we've more than one hotspot, head to the next one...
								new Decorator(ret => (_hotSpots.Count() > 1),
									new Sequence(context => FindNextHotspot(),
										new Action(nextHotspot => TreeRoot.StatusText = "No targets--moving to "
																						+ ((WoWPointNamed)nextHotspot).Name),
										CreateBehavior_InternalMoveTo(() => FindNextHotspot())
										)),

								// We find a point 'near' our anchor at which to wait...
				// This way, if multiple people are using the same profile at the same time,
				// they won't be standing on top of each other.
								new Decorator(ret => (_huntingGroundWaitPoint.Location == WoWPoint.Empty),
									new Action(delegate
										{
											_huntingGroundWaitPoint.Location = HuntingGroundAnchor.Location.FanOutRandom(CollectionDistance * 0.25);
											TreeRoot.StatusText = "No targets--moving to wait near " + _huntingGroundWaitPoint.Name;
											_repopWaitingTime.Reset();
											_repopWaitingTime.Start();
										})),

								// Move to our selected random point...
								new Decorator(ret => !Navigator.AtLocation(_huntingGroundWaitPoint.Location),
									CreateBehavior_InternalMoveTo(() => _huntingGroundWaitPoint)),

								// Tell user what's going on...
								new Sequence(
									new Action(delegate
										{
											TreeRoot.GoalText = this.GetType().Name + ": Waiting for Repops";
											TreeRoot.StatusText = "No targets in area--waiting for repops.  " + BuildTimeAsString(_repopWaitingTime.Elapsed);
										}),
									new WaitContinue(Delay_RepopWait, ret => false, new ActionAlwaysSucceed()))
								))
						)),

				// Re-select target, if it was lost (perhaps, due to combat)...
				new Decorator(ret => ((CurrentTarget is WoWUnit) && (Me.CurrentTarget != CurrentTarget)),
					new Action(delegate { CurrentTarget.ToUnit().Target(); }))
				));
		}


		public Composite CreateBehavior_MoveNearTarget(WoWObjectDelegate target,
														  DistanceDelegate minRange,
														  DistanceDelegate maxRange)
		{
			return (
			new PrioritySelector(context => target(),

				// If we're too far from target, move closer...
				new Decorator(wowObject => (((WoWObject)wowObject).Distance > maxRange()),
					new Sequence(
						new DecoratorThrottled(Delay_StatusUpdateThrottle, ret => true,
							new Action(wowObject =>
							{
								TreeRoot.StatusText = string.Format("Moving to {0} (distance: {1:0.0}) ",
                                                                    ((WoWObject)wowObject).SafeName,
																	((WoWObject)wowObject).Distance);
							})),

						CreateBehavior_InternalMoveTo(() => new WoWPointNamed(target().Location))
					)),

				// If we're too close to target, back up...
				new Decorator(wowObject => (((WoWObject)wowObject).Distance < minRange()),
					new PrioritySelector(

						// If backing up, make sure we're facing the target...
						new Decorator(ret => Me.MovementInfo.MovingBackward,
							new Action(wowObject => WoWMovement.Face(((WoWObject)wowObject).Guid))),

						// Start backing up...
						new Action(wowObject =>
						{
                            TreeRoot.StatusText = "Too close to \"" + ((WoWObject)wowObject).SafeName + "\"--backing up";
							WoWMovement.MoveStop();
							WoWMovement.Face(((WoWObject)wowObject).Guid);
							WoWMovement.Move(WoWMovement.MovementDirection.Backwards);
						})
						)),

				// We're between MinRange and MaxRange, stop movement and face the target...
				new Decorator(ret => Me.IsMoving,
					new Sequence(
						new Action(delegate { WoWMovement.MoveStop(); }),
						new Action(wowObject => WoWMovement.Face(((WoWObject)wowObject).Guid)),
						new WaitContinue(Delay_WowClientLagTime, ret => false, new ActionAlwaysSucceed()),
						new ActionAlwaysFail()      // fall through to next element
						))
			));
		}


		public Composite CreateBehavior_MoveToTarget()
		{
			return (CreateBehavior_MoveToTarget(() => CurrentTarget));
		}


		public Composite CreateBehavior_MoveToTarget(WoWObjectDelegate target)
		{
			return (
			new PrioritySelector(context => target(),

				// If we 'pass by' the current hotspot on the way to the target, advance to next hotspot...
				// This prevents bot-like 'back tracking'.
				new Decorator(ret => (Me.Location.Distance(_hotSpots.Peek().Location) < (CollectionDistance / 2)),
					new Action(delegate
					{
						// Rotate to the next hotspot in the list...
						_hotSpots.Enqueue(_hotSpots.Peek());
						_hotSpots.Dequeue();

						return (RunStatus.Failure);     // Fall through to next
					})),


				// If we're not at target, move to it...
				new Decorator(wowObject => (((WoWObject)wowObject).Distance > ((WoWObject)wowObject).InteractRange),
					new Sequence(
						new DecoratorContinueThrottled(Delay_StatusUpdateThrottle, ret => true,
							new Action(wowObject =>
							{
								TreeRoot.StatusText = string.Format("Moving to {0} (distance: {1:0.0}) ",
                                                                    ((WoWObject)wowObject).SafeName,
																	((WoWObject)wowObject).Distance);
							})),

						CreateBehavior_InternalMoveTo(() => new WoWPointNamed(target().Location))
					)),


				// Update status...
				new Action(wowObject =>
					{
						TreeRoot.StatusText = string.Format("Moving to {0} (distance: {1:0.0}) ",
                                                            ((WoWObject)wowObject).SafeName,
															((WoWObject)wowObject).Distance);
						return (RunStatus.Failure);
					})
			));
		}


		private static string BuildTimeAsString(TimeSpan timeSpan)
		{
			string formatString = string.Empty;

			if (timeSpan.Hours > 0)
			{ formatString = "{0:D2}h:{1:D2}m:{2:D2}s"; }
			else if (timeSpan.Minutes > 0)
			{ formatString = "{1:D2}m:{2:D2}s"; }
			else
			{ formatString = "{2:D2}s"; }

			return (string.Format(formatString, timeSpan.Hours, timeSpan.Minutes, timeSpan.Seconds));
		}


		private TimeSpan CalculateAutoBlacklistTime(WoWObject wowObject)
		{
			double timeToWowObject;

			if (Me.IsSwimming)
			{ timeToWowObject = Me.Location.Distance(wowObject.Location) / Me.MovementInfo.SwimmingForwardSpeed; }
			else
			{ timeToWowObject = Me.Location.SurfacePathDistance(wowObject.Location) / Me.MovementInfo.RunSpeed; }

			timeToWowObject *= 2.5;     // factor of safety
			timeToWowObject = Math.Max(timeToWowObject, 20.0);  // 20sec hard lower-limit

			return (TimeSpan.FromSeconds(timeToWowObject));
		}


		private Composite CreateBehavior_InternalMoveTo(LocationDelegate locationDelegate)
		{
			return (
			new Sequence(context => locationDelegate(),

				// Druids, switch to Aquatic Form if swimming and distance dictates...
				new DecoratorContinue(ret => (SpellManager.CanCast(SpellId_DruidAquaticForm)
												&& !Me.HasAura(AuraName_DruidAquaticForm)
												&& (Me.Location.Distance(locationDelegate().Location) > MinDistanceToUse_DruidAquaticForm)),
					new Action(delegate { SpellManager.Cast(SpellId_DruidAquaticForm); })),

				// Move...
				new Action(delegate
				{
					// Try to use Navigator to get there...
					WoWPointNamed destination = locationDelegate();
					MoveResult moveResult = Navigator.MoveTo(destination.Location);

					// If Navigator fails, fall back to click-to-move...
					if ((moveResult == MoveResult.Failed) || (moveResult == MoveResult.PathGenerationFailed))
					{ WoWMovement.ClickToMove(destination.Location); }
				}),

				new WaitContinue(Delay_WoWClientMovementThrottle, ret => false, new ActionAlwaysSucceed())
				)
			);
		}


		private WoWPointNamed FindStartingHotspot(bool randomStartingHotspot)
		{
			IEnumerable<WoWPointNamed> hotspotsByDistance;
			IEnumerable<WoWPointNamed> hotspotsStarting;
			Random random = new Random((int)DateTime.Now.Ticks);

			hotspotsByDistance = (Me.IsSwimming
								  ? _hotSpots.OrderBy(hotspot => hotspot.Location.Distance(Me.Location))
								  : _hotSpots.OrderBy(hotspot => hotspot.Location.SurfacePathDistance(Me.Location)));

			hotspotsStarting = hotspotsByDistance.Where(hotspot => (hotspot.IsStarting == true));
			if (hotspotsStarting.Count() <= 0)
			{
				hotspotsStarting = hotspotsByDistance;
				QBCLog.DeveloperInfo("No explicit starting hotspot(s)--considering all");
			}

			QBCLog.DeveloperInfo("Hotspot count: {0} ({1})", hotspotsByDistance.Count(),
						(randomStartingHotspot ? "randomized" : "starting at nearest"));

			WoWPoint startingLocation = (randomStartingHotspot
												? hotspotsStarting.OrderBy(ret => random.Next()).FirstOrDefault().Location
												: hotspotsStarting.FirstOrDefault().Location);

			// Rotate the hotspot queue such that the nearest hotspot is on top...
			while (_hotSpots.Peek().Location != startingLocation)
			{ _hotSpots.Enqueue(_hotSpots.Dequeue()); }

			QBCLog.DeveloperInfo("Starting hotspot is {0}", _hotSpots.Peek().Name);

			return (_hotSpots.Peek());
		}


		private WoWPointNamed FindNextHotspot()
		{
			WoWPointNamed currentHotspot = _hotSpots.Peek();

			// If we haven't reached the current hotspot, it is still the 'next' one...
			if (!Navigator.AtLocation(currentHotspot.Location))
				{ return (currentHotspot); }

			// Otherwise, rotate to the next hotspot in the list...
			_hotSpots.Enqueue(_hotSpots.Dequeue());

			return (_hotSpots.Peek());
		}


		public void UseHotspots(IEnumerable<WoWPointNamed> hotspots,
										bool randomizeStartingHotspot)
		{
			_hotSpots = new Queue<WoWPointNamed>(hotspots ?? new WoWPointNamed[0]);

			if (_hotSpots.Count() <= 0)
			{ _hotSpots.Enqueue(HuntingGroundAnchor); }

			FindStartingHotspot(randomizeStartingHotspot);
		}
	}


	/// <summary>
	/// <para>This behavior moves to fill a toon's lungs when needed.  It utilizes skills available
	/// such as a Warlock's Unending Breath, or a Druid's Aquatic Form in its calculations.
	/// The behavior looks for nearby air sources (underwater vents, water's surface, etc), and
	/// bases its time to move upon the distance that needs to be traveled to get to the air source
	/// and the toon's movement speed in the water.</para>
	/// <para>This behavior will *not* use engineering and other devices that may allow you to
	/// breath--as this could interfere with AutoEquip-type plugins.  This behavior will also
	/// *not* use potions to help you breathe, as that is a 'profile-level' decision that should
	/// be made.  In other words, you wouldn't want this behavior to burn a potion to collect
	/// a handful of object from shallow, but swimmable water.  The profile would know the expected
	/// duration underwater, but there is no way for this behavior to know such information.</para>
	/// </summary>
	//
	// Usage:
	//  private SwimBreathBehavior   _swimBreathBehavior  = new SwimBreathBehavior();
	//  ...
	//  new PrioritySelector(
	//      _swimBreathBehavior.CreateBehavior(),
	//
	public class SwimBreathBehavior
	{
		public delegate void LoggerDelegate(string messageType, string format, params object[] args);


		public SwimBreathBehavior()
		{
			// empty
		}


		// Private properites & data...
		private const string AuraName_DruidAquaticForm = "Aquatic Form";
		private const string AuraName_WarlockUnendingBreath = "Unending Breath";
		private int BreathTimeRemaining
		{
			get
			{
				return ((Timer_SwimBreath.IsVisible)
					   ? (int)Timer_SwimBreath.CurrentTime
					   : int.MaxValue);
			}
		}
		private readonly TimeSpan Delay_StatusUpdateThrottle = TimeSpan.FromMilliseconds(3000);
		private int MinTime_DruidBreath = 30000;    // in milliseconds
		private int MinTime_WarlockBreath = 30000;    // in milliseconds
		private LocalPlayer Me { get { return (StyxWoW.Me); } }
		private int SpellId_DruidAquaticForm = 1066;
		private int SpellId_WarlockUnendingBreath = 5697;
		private readonly TimeSpan ThrottleTimer_BreathCheck = TimeSpan.FromSeconds(5);
		private readonly TimeSpan ThrottleTimer_WarlockBreath = TimeSpan.FromSeconds(30);
		private readonly TimeSpan Timer_AuraRefresh_EnduringBreath = TimeSpan.FromMilliseconds(180000);
		private MirrorTimerInfo Timer_SwimBreath { get { return (Me.GetMirrorTimerInfo(MirrorTimerType.Breath)); } }
		private int[] UnderwaterAirSourceObjectIds = { 177524 /* bubbly fissure */
																		  };

		private Composite _behaviorRoot;
		private bool _isSwimBreathNeeded;
		private AirSource _nearestAirSource;

		// Private structures...
		private struct AirSource
		{
			public WoWPoint Location;
			public string Name;

			public AirSource(WoWPoint location, string name) { Location = location; Name = name; }
			public double Distance { get { return (Location.Distance(StyxWoW.Me.Location)); } }
			public static AirSource Empty = new AirSource(WoWPoint.Empty, "NONE");
		}

		// Private LINQs
		private IEnumerable<WoWObject> UnderwaterAirSources
		{
			get
			{
				return (
					ObjectManager.GetObjectsOfType<WoWObject>(true, false)
				.OrderBy(target => Me.Location.Distance(target.Location))
				.Where(target => UnderwaterAirSourceObjectIds.Contains((int)target.Entry))
					);
			}
		}


		private TimeSpan AuraTimeLeft(string auraName)
		{
			WoWAura wowAura = Me.GetAuraByName(auraName);

			return ((wowAura != null) ? wowAura.TimeLeft : TimeSpan.Zero);
		}


		/// <summary>
		/// The created behavior was meant to be used in a PrioritySelector.
		/// It may also have uses inside other TreeSharp Composites.
		/// </summary>
		/// 
		/// <returns>
		/// <para>* RunStatus.Failure, if swim breath is not needed</para>
		/// <para>* RunStatus.Success, if we're catching our breath, or moving for it</para>
		/// </returns>
		/// 
		public Composite CreateBehavior()
		{
			return (_behaviorRoot ?? (_behaviorRoot =
				new Decorator(ret => Me.IsSwimming,
					new PrioritySelector(

						// Moving to, or fetching breath...
						new Decorator(ret => _isSwimBreathNeeded,
							new PrioritySelector(

								// If toon is filling lungs, stay put until full...
								new Decorator(ret => ((Timer_SwimBreath.ChangePerMillisecond > 0)
													  && (Timer_SwimBreath.CurrentTime < Timer_SwimBreath.MaxValue)),
									new Action(delegate
									{
										WoWMovement.MoveStop();
										TreeRoot.StatusText = "Waiting for full breath";
									})),

								// If lungs are full, back to work...
								new Decorator(ret => (Timer_SwimBreath.CurrentTime >= Timer_SwimBreath.MaxValue),
									new Action(delegate { _isSwimBreathNeeded = false; })),

								// Move toon to air source, if needed...
								new Decorator(ret =>
									{
										_nearestAirSource = GetNearestAirSource();
										return !Navigator.AtLocation(_nearestAirSource.Location);
									},
									new Sequence(
										new DecoratorContinueThrottled(Delay_StatusUpdateThrottle, ret => true,
											new Action(delegate
											{
												TreeRoot.StatusText = string.Format("Moving to {0} for breath. (distance {1:0.0})",
																					_nearestAirSource.Name,
																					_nearestAirSource.Distance);
											})),

										new Action(delegate { UnderwaterMoveTo(_nearestAirSource.Location); })
										)
									)
							)),


						// If we're a Warlock, refresh Unending Breath as needed...
						new DecoratorThrottled(ThrottleTimer_WarlockBreath,
							ret => (SpellManager.CanCast(SpellId_WarlockUnendingBreath)
									&& (AuraTimeLeft(AuraName_WarlockUnendingBreath) <= Timer_AuraRefresh_EnduringBreath)),
							new Action(delegate { SpellManager.Cast(SpellId_WarlockUnendingBreath); })),


						// If time to breathe, do something about it...
						new DecoratorThrottled(ThrottleTimer_BreathCheck,
							ret => IsBreathNeeded(),
							new PrioritySelector(

								// If we're a Druid, switch to Aquatic form for breath...
								new Decorator(ret => (SpellManager.CanCast(SpellId_DruidAquaticForm)
													  && !Me.HasAura(AuraName_DruidAquaticForm)),
									new Action(delegate
									{
										SpellManager.Cast(SpellId_DruidAquaticForm);
										TreeRoot.StatusText = "Switching to Aquatic Form for breath";
										_isSwimBreathNeeded = true;
									})),


								// Otherwise, we need to deal with 'normal' way to catch breath...
								new Action(delegate
								{
									_nearestAirSource = GetNearestAirSource();
									QBCLog.Info("Moving to {0} for breath. (distance {1:0.0})",
													_nearestAirSource.Name, _nearestAirSource.Distance);
									_isSwimBreathNeeded = true;
								})
							))
					))));
		}


		private AirSource GetNearestAirSource()
		{
			// Assume water's surface is nearest breath...
			AirSource nearestAirSource = new AirSource(Me.Location.WaterSurface(), "water's surface");
			WoWObject underwaterAirSource = UnderwaterAirSources.FirstOrDefault();

			// If underwater air source exists, and is closer that the water's surface...
			if ((underwaterAirSource != null)
				&& (Me.Location.Distance(underwaterAirSource.Location) <= nearestAirSource.Distance))
			{
				nearestAirSource.Location = underwaterAirSource.Location;
                nearestAirSource.Name = underwaterAirSource.SafeName;
			}

			return (nearestAirSource);
		}


		private bool IsBreathNeeded()
		{
			int breathTimeRemaining = BreathTimeRemaining;

			if (Me.Class == WoWClass.Druid)
			{ return (breathTimeRemaining < MinTime_DruidBreath); }

			else if (Me.Class == WoWClass.Warlock)
			{ return (breathTimeRemaining < MinTime_WarlockBreath); }

			// Calculate time needed to get to an air source...
			AirSource airSource = GetNearestAirSource();
			double travelTime;

			travelTime = (((airSource.Location.Distance(Me.Location) / Me.MovementInfo.SwimmingForwardSpeed)
						  * 2.75)   // factor of safety
						  + (3 * ThrottleTimer_BreathCheck.TotalSeconds));
			travelTime = Math.Min(travelTime, 30.0);    // Hard-minimum of 30secs
			travelTime *= 1000;     // to milliseconds

			return (breathTimeRemaining <= travelTime);
		}


		private void UnderwaterMoveTo(WoWPoint location)
		{
			// Try to use Navigator to get there...
			MoveResult moveResult = Navigator.MoveTo(location);

			// If Navigator fails, resort to click-to-move...
			if ((moveResult == MoveResult.Failed) || (moveResult == MoveResult.PathGenerationFailed))
			{ WoWMovement.ClickToMove(location); }
		}
	}


	/// <summary>
	/// This behavior is necessary since Honorbuddy is incapable of moving underwater.
	/// </summary>
	//
	// Usage:
	//  private UnderwaterLootingBehavior   _underwaterLootingBehavior  = new UnderwaterLootingBehavior();
	//  ...
	//  new PrioritySelector(
	//      _underwaterLootingBehavior.CreateBehavior(),
	//
	public class UnderwaterLootingBehavior
	{
		public delegate bool ForceLootDelegate();
		public delegate void LoggerDelegate(string messageType, string format, params object[] args);


		public UnderwaterLootingBehavior()
		{
			// empty
		}


		// Private properties & data...
		private const string AuraName_DruidAquaticForm = "Aquatic Form";
		private readonly TimeSpan Delay_BlacklistLootedMob = TimeSpan.FromMinutes(7);
		private readonly TimeSpan Delay_WaitForLootCleanup = TimeSpan.FromMilliseconds(5000);
		private TimeSpan Delay_WowClientLagTime { get { return (TimeSpan.FromMilliseconds((StyxWoW.WoWClient.Latency * 2) + 150)); } }
		private readonly TimeSpan Delay_WowClientWaitForLootFrame = TimeSpan.FromSeconds(10);
		private static LocalPlayer Me { get { return (StyxWoW.Me); } }
		private int SpellId_DruidAquaticForm = 1066;

		private Composite _behaviorRoot;
		private WoWObject _currentTarget;

		// Private LINQ...
		private IEnumerable<WoWUnit> LootList
		{
			get
			{
				return (ObjectManager.GetObjectsOfType<WoWUnit>(true, false)
						.Where(target => (target.IsDead && target.Lootable
										  && !target.IsLootingBlacklisted())));
			}
		}


		/// <summary>
		/// The created behavior was meant to be used in a PrioritySelector.
		/// It may also have uses inside other TreeSharp Composites.
		/// </summary>
		/// 
		/// <returns>
		/// <para>* RunStatus.Failure, if looting is not needed</para>
		/// <para>* RunStatus.Success, if we're in the process of looting things</para>
		/// </returns>
		/// 
		public Composite CreateBehavior(ForceLootDelegate forceLoot)
		{
			return (_behaviorRoot ?? (_behaviorRoot =
				new Decorator(ret => ((LootTargeting.LootMobs || forceLoot()) && (LootList.Any())),
					new PrioritySelector(

						// If we're swimming, we need to do loot cleanup for ourselves...
						new Decorator(ret => (Me.IsSwimming || forceLoot()),
							new PrioritySelector(context => _currentTarget = LootList.FirstOrDefault(),

								// If not at nearest target, move to it...
								new Decorator(ret => (_currentTarget.Distance > _currentTarget.InteractRange),
									new Action(delegate
									{
										TreeRoot.StatusText = string.Format("Moving to loot target '{0}' (distance {1})...",
                                                                            _currentTarget.SafeName,
																			_currentTarget.Distance);
										UnderwaterMoveTo(_currentTarget.Location);
									})),

								// Within interact range, so loot it...
				// NOTE: that we have to locally blacklist looted targets.  They sometimes
				// have unique (e.g., quest-starting type) items on them.  If we already have
				// have such an item in our inventory, the target remains lootable, but there
				// is nothing we can pick up from it.  The blacklist prevents us from getting
				// into silly loops because of such mechanics.
								new Sequence(
									new Action(delegate { WoWMovement.MoveStop(); }),
									new WaitContinue(Delay_WowClientLagTime, ret => false, new ActionAlwaysSucceed()),
									new Action(delegate { _currentTarget.Interact(); }),
									new WaitContinue(Delay_WowClientWaitForLootFrame,
													 ret => LootFrame.Instance.IsVisible,
													 new ActionAlwaysSucceed()),
									new DecoratorContinue(ret => LootFrame.Instance.IsVisible,
										new Action(ret => LootFrame.Instance.LootAll())),
									new Action(delegate { _currentTarget.LootingBlacklist(Delay_BlacklistLootedMob); })
									)
							)),

						// We're not swimming, so we want to wait for the 'normal' looting behavior
				// to scoop up the loot before allowing other behaviors to continue...
				// This keeps it from taking a few steps towards next mob, only to go back to
				// previous mob and loot it.  This technique can still fail if Honorbddy is slow to update
				// infomation.  However, it shuts a lot of back-tracking.
						new WaitContinue(Delay_WaitForLootCleanup, ret => false, new ActionAlwaysSucceed())
					)
				)));
		}


		private void UnderwaterMoveTo(WoWPoint location)
		{
			// If we're a Druid, use Aquatic Form...
			if (SpellManager.CanCast(SpellId_DruidAquaticForm) && !Me.HasAura(AuraName_DruidAquaticForm))
			{ SpellManager.Cast(SpellId_DruidAquaticForm); }

			// Try to use Navigator to get there...
			MoveResult moveResult = Navigator.MoveTo(location);

			if ((moveResult == MoveResult.Failed) || (moveResult == MoveResult.PathGenerationFailed))
			{ WoWMovement.ClickToMove(location); }
		}
	}

	#endregion  // Reusable behaviors


	#region Extensions to HBcore

	public class DecoratorContinueThrottled : DecoratorContinue
	{
		public DecoratorContinueThrottled(TimeSpan throttleTime,
										  CanRunDecoratorDelegate canRun,
										  Composite composite)
			: base(canRun, composite)
		{
			_throttleTime = throttleTime;

			_throttle = new Stopwatch();
			_throttle.Reset();
			_throttle.Start();
		}


		protected override bool CanRun(object context)
		{
			if (_throttle.Elapsed < _throttleTime)
			{ return (false); }

			_throttle.Reset();
			_throttle.Start();

			return (base.CanRun(context));
		}


		private Stopwatch _throttle;
		private TimeSpan _throttleTime;
	}


	public class DecoratorThrottled : Decorator
	{
		public DecoratorThrottled(TimeSpan throttleTime,
								  CanRunDecoratorDelegate canRun,
								  Composite composite)
			: base(canRun, composite)
		{
			_throttleTime = throttleTime;

			_throttle = new Stopwatch();
			_throttle.Reset();
			_throttle.Start();
		}


		protected override bool CanRun(object context)
		{
			if (_throttle.Elapsed < _throttleTime)
			{ return (false); }

			_throttle.Reset();
			_throttle.Start();

			return (base.CanRun(context));
		}


		private Stopwatch _throttle;
		private TimeSpan _throttleTime;
	}


	// The HBcore 'global' blacklist will also prevent looting.  We don't want that.
	// Since the HBcore blacklist is not built to instantiate, we have to roll our
	// own.  <sigh>
	public class LocalBlackList
	{
		public LocalBlackList(TimeSpan maxSweepTime)
		{
			_maxSweepTime = maxSweepTime;
			_stopWatchForSweeping.Start();
		}

        private Dictionary<WoWGuid, DateTime> _blackList = new Dictionary<WoWGuid, DateTime>();
		private TimeSpan _maxSweepTime;
		private Stopwatch _stopWatchForSweeping = new Stopwatch();


        public void Add(WoWGuid guid, TimeSpan timeSpan)
		{
			if (_stopWatchForSweeping.Elapsed > _maxSweepTime)
			{ RemoveExpired(); }

			_blackList[guid] = DateTime.Now.Add(timeSpan);
		}


        public bool Contains(WoWGuid guid)
		{
			if (_stopWatchForSweeping.Elapsed > _maxSweepTime)
			{ RemoveExpired(); }

			return (_blackList.ContainsKey(guid));
		}


		public void RemoveExpired()
		{
			DateTime now = DateTime.Now;

            List<WoWGuid> expiredEntries = (from key in _blackList.Keys
										  where (_blackList[key] < now)
										  select key).ToList();

            foreach (WoWGuid entry in expiredEntries)
			{ _blackList.Remove(entry); }

			_stopWatchForSweeping.Reset();
			_stopWatchForSweeping.Start();
		}
	}


	public static class WoWObject_Extensions
	{
		private static LocalPlayer Me { get { return (StyxWoW.Me); } }

		// We provide our own 'local' blacklist.  If we use the global one maintained by HBcore,
		// that will prevent us from looting also.
		private static LocalBlackList _blackList = new LocalBlackList(TimeSpan.FromSeconds(30));
		private static LocalBlackList _blackListLooting = new LocalBlackList(TimeSpan.FromSeconds(30));

		public static void LocallyBlacklist(this WoWObject wowObject,
												 TimeSpan timeSpan)
		{
			_blackList.Add(wowObject.Guid, timeSpan);
		}

		public static void LootingBlacklist(this WoWObject wowObject,
												 TimeSpan timeSpan)
		{
			_blackListLooting.Add(wowObject.Guid, timeSpan);
		}


		public static bool IsLocallyBlacklisted(this WoWObject wowObject)
		{
			return (_blackList.Contains(wowObject.Guid));
		}

		public static bool IsLootingBlacklisted(this WoWObject wowObject)
		{
			return (_blackListLooting.Contains(wowObject.Guid));
		}
	}


	public static class WoWUnit_Extensions
	{
		private static LocalPlayer Me { get { return (StyxWoW.Me); } }

		public static bool IsInOurParty(this WoWUnit wowUnit)
		{
			return ((Me.PartyMembers.FirstOrDefault(partyMember => (partyMember.Guid == wowUnit.Guid))) != null);
		}
	}


	public static class WoWPoint_Extensions
	{
		public static Random _random = new Random((int)DateTime.Now.Ticks);

		private static LocalPlayer Me { get { return (StyxWoW.Me); } }
		public const double TAU = (2 * Math.PI);    // See http://tauday.com/


		public static WoWPoint Add(this WoWPoint wowPoint,
									double x,
									double y,
									double z)
		{
			return (new WoWPoint((wowPoint.X + x), (wowPoint.Y + y), (wowPoint.Z + z)));
		}


		public static WoWPoint AddPolarXY(this WoWPoint wowPoint,
										   double xyHeadingInRadians,
										   double distance,
										   double zModifier)
		{
			return (wowPoint.Add((Math.Cos(xyHeadingInRadians) * distance),
								 (Math.Sin(xyHeadingInRadians) * distance),
								 zModifier));
		}


		// Finds another point near the destination.  Useful when toon is 'waiting' for something
		// (e.g., boat, mob repops, etc). This allows multiple people running
		// the same profile to not stand on top of each other while waiting for
		// something.
		public static WoWPoint FanOutRandom(this WoWPoint location,
												double maxRadius)
		{
			const int CYLINDER_LINE_COUNT = 12;
			const int MAX_TRIES = 50;
			const double SAFE_DISTANCE_BUFFER = 1.75;

			WoWPoint candidateDestination = location;
			int tryCount;

			// Most of the time we'll find a viable spot in less than 2 tries...
			// However, if you're standing on a pier, or small platform a
			// viable alternative may take 10-15 tries--its all up to the
			// random number generator.
			for (tryCount = MAX_TRIES; tryCount > 0; --tryCount)
			{
				WoWPoint circlePoint;
				bool[] hitResults;
				WoWPoint[] hitPoints;
				int index;
				WorldLine[] traceLines = new WorldLine[CYLINDER_LINE_COUNT + 1];

				candidateDestination = location.AddPolarXY((TAU * _random.NextDouble()), (maxRadius * _random.NextDouble()), 0.0);

				// Build set of tracelines that can evaluate the candidate destination --
				// We build a cone of lines with the cone's base at the destination's 'feet',
				// and the cone's point at maxRadius over the destination's 'head'.  We also
				// include the cone 'normal' as the first entry.

				// 'Normal' vector
				index = 0;
				traceLines[index].Start = candidateDestination.Add(0.0, 0.0, maxRadius);
				traceLines[index].End = candidateDestination.Add(0.0, 0.0, -maxRadius);

				// Cylinder vectors
				for (double turnFraction = 0.0; turnFraction < TAU; turnFraction += (TAU / CYLINDER_LINE_COUNT))
				{
					++index;
					circlePoint = candidateDestination.AddPolarXY(turnFraction, SAFE_DISTANCE_BUFFER, 0.0);
					traceLines[index].Start = circlePoint.Add(0.0, 0.0, maxRadius);
					traceLines[index].End = circlePoint.Add(0.0, 0.0, -maxRadius);
				}


				// Evaluate the cylinder...
				// The result for the 'normal' vector (first one) will be the location where the
				// destination meets the ground.  Before this MassTrace, only the candidateDestination's
				// X/Y values were valid.
				GameWorld.MassTraceLine(traceLines.ToArray(),
                                        TraceLineHitFlags.Collision,
										out hitResults,
										out hitPoints);

				candidateDestination = hitPoints[0];    // From 'normal', Destination with valid Z coordinate


				// Sanity check...
				// We don't want to be standing right on the edge of a drop-off (say we'e on
				// a plaform or pier).  If there is not solid ground all around us, we reject
				// the candidate.  Our test for validity is that the walking distance must
				// not be more than 20% greater than the straight-line distance to the point.
				int viableVectorCount = hitPoints.Sum(point => ((Me.Location.SurfacePathDistance(point) < (Me.Location.Distance(point) * 1.20))
																	  ? 1
																	  : 0));

				if (viableVectorCount < (CYLINDER_LINE_COUNT + 1))
				{ continue; }

				// If new destination is 'too close' to our current position, try again...
				if (Me.Location.Distance(candidateDestination) <= SAFE_DISTANCE_BUFFER)
				{ continue; }

				break;
			}

			// If we exhausted our tries, just go with simple destination --
			if (tryCount <= 0)
			{ candidateDestination = location; }

			return (candidateDestination);
		}


		public static double SurfacePathDistance(this WoWPoint start,
													WoWPoint destination)
		{
			WoWPoint[] groundPath = Navigator.GeneratePath(start, destination) ?? new WoWPoint[0];

			// We define an invalid path to be of 'infinite' length
			if (groundPath.Length <= 0)
			{ return (double.MaxValue); }


			double pathDistance = start.Distance(groundPath[0]);

			for (int i = 0; i < (groundPath.Length - 1); ++i)
			{ pathDistance += groundPath[i].Distance(groundPath[i + 1]); }

			return (pathDistance);
		}


		// Returns WoWPoint.Empty if unable to locate water's surface
		public static WoWPoint WaterSurface(this WoWPoint location)
		{
			WoWPoint hitLocation;
			bool hitResult;
			WoWPoint locationUpper = location.Add(0.0, 0.0, 2000.0);
			WoWPoint locationLower = location.Add(0.0, 0.0, -2000.0);

			hitResult = GameWorld.TraceLine(locationUpper,
											 locationLower,
                                             TraceLineHitFlags.LiquidAll,
											 out hitLocation);

			return (hitResult ? hitLocation : WoWPoint.Empty);
		}
	}

	#endregion      // Extensions to HBcore
}

